﻿using System;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Kyoh.Lib.IO.Tasks.Events;

namespace Kyoh.Lib.IO.Tasks
{
    /// <summary>ファイル・ディレクトリの非同期処理を提供します。</summary>
    /// <typeparam name="TProfile">ファイル・ディレクトリ一つ一つに対する処理の詳細な内容を表すオブジェクトの型。</typeparam>
    /// <typeparam name="TProcessor">ファイル・ディレクトリ一つ一つに処理を行う <see cref="IFileProcessor{TProfile}"/> インタフェースを実装したクラスの型。</typeparam>
    public partial class FileTaskProcessor<TProfile, TProcessor> : Component
        where TProcessor : IFileProcessor<TProfile>
    {
        private readonly object locker;
        private FileTaskQueue<TProfile> queue;
        private ManualResetEvent reset;
        private Task task;

        /// <summary>ファイル・ディレクトリの処理を行う <see cref="IFileProcessor{TProfile}"/> オブジェクトを取得します。</summary>
        public IFileProcessor<TProfile> Processor { get; private set; }

        /// <summary><see cref="FileTaskProcessor{TProfile, TProcessor}"/> クラスの新しいインスタンスを初期化します。</summary>
        /// <param name="processor">ファイル・ディレクトリの処理を行う <see cref="IFileProcessor{TProfile}"/> オブジェクト。</param>
        public FileTaskProcessor(IFileProcessor<TProfile> processor)
        {
            this.Processor = processor;
            lock (this.locker = new object())
            {
                this.queue = new FileTaskQueue<TProfile>();
                this.reset = new ManualResetEvent(false);
                this.task = Task.Factory.StartNew(this.DoTask);
            }
        }

        #region 非同期処理

        /// <summary>新しいパスを追加して、非同期処理を開始します。</summary>
        /// <param name="target">処理対象のファイルと、処理内容を格納した <see cref="FileTask{TProfile}"/>。</param>
        /// <returns>追加されたパスを処理するタスクの <see cref="Task"/>。</returns>
        public Task Entry(FileTask<TProfile> target)
        {
            bool err = false;
            try
            {
                if (File.Exists(target.Path))
                {
                    this.queue.Enqueue(new FileTask<TProfile>(target.Path, target.Pattern, target.Recursive, false, target.Profile));
                    this.RaiseFileProgress(true);
                }
                else if (Directory.Exists(target.Path))
                {
                    this.queue.Enqueue(new FileTask<TProfile>(target.Path, target.Pattern, target.Recursive, true, target.Profile));
                    this.RaiseDirectoryProgress(true);
                }
                else
                    throw new FileNotFoundException(string.Empty, target.Path);
                lock (this.locker)
                    return this.task;
            }
            catch
            {
                err = true;
                throw;
            }
            finally
            {
                if (!err)
                    this.reset.Set();
            }
        }

        private void DoTask()
        {
            this.reset.WaitOne();
            this.reset.Reset();
            this.RaiseStarted();
            try
            {
                foreach (var target in this.queue)
                {
                    lock (this.locker)
                        if (this.IsCanceled)
                            throw new SuspendException();
                    if (target.IsDirectory)
                    {
                        try { this.DoTaskDirectory(target); }
                        catch (SuspendException) { throw; }
                        catch (Exception exc) { this.RaiseExceptionThrown(exc); }
                        this.RaiseDirectoryProgress(false);
                    }
                    else
                    {
                        try { this.DoTaskFile(target); }
                        catch (SuspendException) { throw; }
                        catch (Exception exc) { this.RaiseExceptionThrown(exc); }

                        this.RaiseFileProgress(false);
                    }
                }
                this.RaiseComplete(ExitReason.Complete);
            }
            catch (SuspendException) { this.RaiseComplete(ExitReason.Cancel); }
            
            lock (this.locker)
                this.task = Task.Factory.StartNew(this.DoTask);
        }

        /// <summary>タスクがキャンセルされたかどうかを示す値を取得します。</summary>
        public bool IsCanceled { get; private set; }

        /// <summary>実行中の非同期処理を中断します。</summary>
        public void Cancel()
        {
            lock (this.locker)
                this.IsCanceled = true;
        }

        #endregion

        #region ファイルに対する処理本体

        private void DoTaskDirectory(FileTask<TProfile> target)
        {
            foreach (var pattern in target.Pattern.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
            {
                lock (this.locker)
                    if (this.IsCanceled)
                        throw new SuspendException();
                foreach (var file in Directory.EnumerateFiles(target.Path, pattern, target.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
                {
                    lock (this.locker)
                        if (this.IsCanceled)
                            throw new SuspendException();
                    this.Entry(new FileTask<TProfile>(file, target.Pattern, target.Recursive, target.Profile));
                }
            }
        }

        private void DoTaskFile(FileTask<TProfile> target)
        {
            try
            {
                this.Processor.Process(target);
            }
            catch (Exception exc)
            {
                this.RaiseExceptionThrown(exc);
            }
        }

        #endregion

        #region 通知イベント

        private int directoryDone;
        private int directoryTotal;
        private int fileDone;
        private int fileTotal;

        /// <summary>処理が開始された際のイベントを追加・削除します。</summary>
        public event EventHandler Started;
        /// <summary><see cref="Started"/> イベントを発生させます。</summary>
        /// <param name="e">イベントの引数。</param>
        protected virtual void OnStarted(EventArgs e)
        {
            if (this.Started != null)
                this.Started(this, EventArgs.Empty);
        }
        private void RaiseStarted()
        {
            this.OnStarted(EventArgs.Empty);
        }

        /// <summary>ディレクトリの処理の進捗が変化した際のイベントを追加・削除します。</summary>
        public event EventHandler<ProgressEventArgs> DirectoryProgress;
        private void RaiseDirectoryProgress(bool total)
        {
            ProgressEventArgs e;
            lock (this.locker)
            {
                if (total)
                    this.directoryTotal++;
                else
                    this.directoryDone++;
                e = new ProgressEventArgs() { Done = this.directoryDone, Total = this.directoryTotal };
            }
            if (this.DirectoryProgress != null)
                this.DirectoryProgress(this, e);
        }

        /// <summary>ファイルの処理の進捗が変化した際のイベントを追加・削除します。</summary>
        public event EventHandler<ProgressEventArgs> FileProgress;
        private void RaiseFileProgress(bool total)
        {
            ProgressEventArgs e;
            lock (this.locker)
            {
                if (total)
                    this.fileTotal++;
                else
                    this.fileDone++;
                e = new ProgressEventArgs() { Done = this.fileDone, Total = this.fileTotal };
            }
            if (this.FileProgress != null)
                this.FileProgress(this, e);
        }

        /// <summary>処理が終了した際のイベントを追加・削除します。</summary>
        public event EventHandler<ExitEventArgs> Complete;
        private void RaiseComplete(ExitReason reason)
        {
            lock (this.locker)
            {
                this.IsCanceled = false;
                this.queue.Clear();

                this.directoryDone = 0;
                this.directoryTotal = 0;
                this.fileDone = 0;
                this.fileTotal = 0;
            }
            var e = new ExitEventArgs() { Reason = reason };
            if (this.Complete != null)
                this.Complete(this, e);
        }

        /// <summary>処理中に例外が発生した際のイベントを追加・削除します。</summary>
        public event EventHandler<ExceptionEventArgs> ExceptionThrown;
        private void RaiseExceptionThrown(Exception exception)
        {
            var e = new ExceptionEventArgs() { Exception = exception };
            if (this.ExceptionThrown != null)
                this.ExceptionThrown(this, e);
        }

        #endregion
    }
}